﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MetaCollection.Framework.Model;
using System.Globalization;
using System.Text.RegularExpressions;
using MetaCollection.Framework.Security;

namespace MetaCollection.Framework.Index
{
	/// <summary>
	/// This class takes item specifications and item instances and creates
	/// Lucene index documents from them.  It does not index the item resource.
	/// Additinoal document factories need to be used to index the resource.
	/// </summary>
	public class ItemSpecificationDocumentFactory : IDocumentFactory
	{
		/// <summary>
		/// handler to use to request documents
		/// </summary>
		IRequestHandler m_Handler;
		/// <summary>
		/// System principal to use for making requests of the collection during indexing.
		/// </summary>
		MetaPrincipal m_SystemUser = MetaPrincipal.SystemPrincipal;
		/// <summary>
		/// collection of specifications encountered during processing, reused for best performance.
		/// </summary>
		IDictionary<Guid, IItemSpecification> m_Specs = new Dictionary<Guid, IItemSpecification>();

		/// <summary>
		/// Creates a new ItemSpecificationDocumentFactory but does not set 
		/// the IRequestHandler so the factory is only useful for creating other
		/// factories using the CreateFactory method on the IDocumentFactory interface
		/// </summary>
		public ItemSpecificationDocumentFactory() { }

		/// <summary>
		/// Creates a new ItemSpecificationDocumentFactory using the given handler.
		/// </summary>
		/// <param name="handler">the request handler to use to resolve item uris into items.</param>
		public ItemSpecificationDocumentFactory(IRequestHandler handler)
		{
			if (handler == null)
				throw new ArgumentNullException("handler", "An IRequestHandler is required");
			m_Handler = handler;
		}

		#region IDocumentFactory Members
		/// <summary>
		/// Creates a new Item Specification Document Handler.
		/// </summary>
		/// <param name="handler"></param>
		/// <returns></returns>
		public IDocumentFactory CreateFactory(IRequestHandler handler)
		{
			return new ItemSpecificationDocumentFactory(handler);
		}

		/// <summary>
		/// Creates a Lucene Index document for a given item.
		/// </summary>
		/// <param name="itemUri"></param>
		/// <returns>a lucene document for the given item</returns>
		[CLSCompliant(false)]
		public IEnumerable<Lucene.Net.Documents.Document> CreateDocument(Uri itemUri, bool includeVersions = false)
		{
			var items = GetItems(itemUri, includeVersions);
			if (items == null)
				yield break;

			foreach (var item in items)
			{
				var spec = GetSpecification(item);
				var doc = new Lucene.Net.Documents.Document();

				foreach (var prop in EnsureDefaults(spec.Properties))
				{
					var val = GetPropertyValue(prop, item);
					if (string.IsNullOrWhiteSpace(val))
						continue;
					var field = new Lucene.Net.Documents.Field(
						prop.Name,
						val,
						FindStore(prop.Store),
						FindIndex(prop.Style),
						FindVector(prop.Vector));
					doc.Add(field);
				}
				yield return doc;				
			}
		}

		/// <summary>
		/// Converts from the model IndexVector terms to Lucene Field.TermVector values
		/// </summary>
		/// <param name="indexVector">the model value</param>
		/// <returns>the lucene value</returns>
		static Lucene.Net.Documents.Field.TermVector FindVector(IndexVector indexVector)
		{
			switch (indexVector)
			{
				case IndexVector.No: return Lucene.Net.Documents.Field.TermVector.NO;
				case IndexVector.WithOffsets: return Lucene.Net.Documents.Field.TermVector.WITH_OFFSETS;
				case IndexVector.WithPositions: return Lucene.Net.Documents.Field.TermVector.WITH_POSITIONS;
				case IndexVector.WithPositionsOffsets: return Lucene.Net.Documents.Field.TermVector.WITH_POSITIONS_OFFSETS;
				case IndexVector.Yes: return Lucene.Net.Documents.Field.TermVector.YES;
				default: throw new InvalidOperationException("Unexpected value for Index Vector encountered.");
			}
		}

		/// <summary>
		/// Converts from the model IndexStyle terms to Lucene Field.Index values
		/// </summary>
		/// <param name="indexStyle">the model value</param>
		/// <returns>the lucene value</returns>
		static Lucene.Net.Documents.Field.Index FindIndex(IndexStyle indexStyle)
		{
			switch (indexStyle)
			{
				case IndexStyle.Analyzed: return Lucene.Net.Documents.Field.Index.ANALYZED;
				case IndexStyle.AnalyzedNoNorms: return Lucene.Net.Documents.Field.Index.ANALYZED_NO_NORMS;
				case IndexStyle.No: return Lucene.Net.Documents.Field.Index.NO;
				case IndexStyle.NotAnalyzed: return Lucene.Net.Documents.Field.Index.NOT_ANALYZED;
				case IndexStyle.NotAnalyzedNoNorms: return Lucene.Net.Documents.Field.Index.NOT_ANALYZED_NO_NORMS;
				default: throw new InvalidOperationException("Unexpected value for Index Style encountered.");
			}
		}

		/// <summary>
		/// Converts from the model IndexStore terms to Lucene Field.Store values
		/// </summary>
		/// <param name="indexStore">the model value</param>
		/// <returns>the lucene value</returns>
		static Lucene.Net.Documents.Field.Store FindStore(IndexStore indexStore)
		{
			switch (indexStore)
			{
				case IndexStore.Compress: return Lucene.Net.Documents.Field.Store.COMPRESS;
				case IndexStore.No: return Lucene.Net.Documents.Field.Store.NO;
				case IndexStore.Yes: return Lucene.Net.Documents.Field.Store.YES;
				default: throw new InvalidOperationException("Unexpected value for Index Store encountered.");
			}
		}

		/// <summary>
		/// Returns the string representation of the value of a property based on a given item.
		/// </summary>
		/// <param name="prop">the property to look for</param>
		/// <param name="item">the item to find the property of</param>
		/// <returns>the value of the property</returns>
		static string GetPropertyValue(IModelProperty prop, IItemInstance item)
		{
			string propertyValue = String.Empty;
			if (prop == null)
				throw new ArgumentNullException("prop");
			if (item == null)
				throw new ArgumentNullException("item");
			if (item.Properties == null) // seems like this should be an empty enumerable and not a null value but this protects us.
				return null;
			if (prop.IsExtended)
				propertyValue = item.Properties.Where(p => p.KeyName.Equals(prop.Name, StringComparison.OrdinalIgnoreCase)).Select(p => p.KeyValue).FirstOrDefault();
			else
			{
				switch (prop.Name)
				{
					case "Id": propertyValue = item.Id.ToString(); break;
					case "ParentId": propertyValue = item.ParentId.ToString(); break;
					case "ExternalId": propertyValue = item.ExternalId; break;
					case "Version": propertyValue = item.Version.ToString(CultureInfo.CurrentCulture); break;
					case "LeftVersion": propertyValue = item.LeftVersion.HasValue ? item.LeftVersion.Value.ToString(CultureInfo.CurrentCulture) : String.Empty; break;
					case "RightVersion": propertyValue = item.RightVersion.HasValue ? item.RightVersion.Value.ToString(CultureInfo.CurrentCulture) : String.Empty; break;
					case "Comment": propertyValue = item.Comment; break;
					case "CreatedBy": propertyValue = item.CreatedBy; break;
					case "Name": propertyValue = item.Name; break;
					case "Href": propertyValue = item.Href.ToString(); break;
					case "LastModified": propertyValue = item.LastModified.ToString(CultureInfo.CurrentCulture); break;
					case "MimeType": propertyValue = item.MimeType; break;
					case "Published": propertyValue = item.Published.ToString(); break;
					case "Specification": propertyValue = item.Specification.ToString(); break;
					case "ItemClaimId": propertyValue = item.ItemClaimId.ToString(); break;
					case "Relationships": propertyValue = GetSanitizedTarget(item.Relationships); break;
					default: break;
				}
			}
			return propertyValue;
			
		}

		/// <summary>
		/// Gets the sanitized target.
		/// </summary>
		/// <param name="relationships">The relationships.</param>
		/// <returns></returns>
		private static string GetSanitizedTarget(IEnumerable<IItemRelationship> relationships) 
		{
			Regex regex = new Regex("[^a-zA-Z0-9]", RegexOptions.IgnoreCase);
			StringBuilder sb = new StringBuilder();
			foreach (Uri target in relationships.Select(p=>p.TargetItem))
			{
				sb.Append(" ");
				sb.Append(regex.Replace(target.ToString(), string.Empty));
			}
			return sb.ToString().ToLowerInvariant();
		}

		/// <summary>
		/// Returns the item specification for a given item.
		/// </summary>
		/// <param name="item">the item to get the specification for.  Item Href must not be null.</param>
		/// <returns></returns>
		IItemSpecification GetSpecification(IItemInstance item)
		{
			IItemSpecification specification = null;

			if (item == null || item.Href == null)
				throw new ArgumentNullException("item");

			if (m_Handler == null)
				throw new InvalidOperationException("Can not get specification because the factory was not initialized or created with a reference to the collection.");

			if (m_Specs.ContainsKey(item.Specification))
				specification = m_Specs[item.Specification];
			else
			{
				specification = m_Handler.ItemSpecificationRequest(m_SystemUser, item.Href);
				if (specification != null && !m_Specs.ContainsKey(specification.Id)) // check again since we are not checking the same thing as a key
					m_Specs.Add(specification.Id, specification);
				else
					specification = new ItemSpecification();
			}
			return specification;
		}

		/// <summary>
		/// Walks through a given set of properties and ensures that the returned set
		/// includes all of the basic defaults.
		/// </summary>
		/// <param name="specificationProperties">a list of specification model properties (or empty)</param>
		/// <returns>a set of properties from the specification unioned with the required defaults for indexing</returns>
		IEnumerable<IModelProperty> EnsureDefaults(IEnumerable<IModelProperty> specificationProperties)
		{
			List<string> visitedKeys = new List<string>();
			foreach(var p in DefaultProperties())
			{
				yield return p;
				visitedKeys.Add(p.Name);
			}
			foreach (var p in specificationProperties)
			{
				if (!visitedKeys.Contains(p.Name))
					yield return p;
			}
		}

		/// <summary>
		/// Returns the default properties that need to be index on each item regardless of Specification.
		/// </summary>
		/// <returns>a list of default properties to index.</returns>
		static IEnumerable<IModelProperty> DefaultProperties()
		{
			string[] props = "Id,ParentId,Version,Published,Name,Href,LastModified,MimeType,Specification,ItemClaimId,ExternalId,Relationships".Split(',');
			foreach (var name in props)
			{
				var prop = new ModelProperty { Name = name, IsExtended = false, Store = IndexStore.Yes, Vector = IndexVector.No, Style = IndexStyle.NotAnalyzed };
				if (name == "Name" || name == "Href" || name == "MimeType" || name == "Relationships") // we analyze these so they are broken into tokens.
					prop.Style = IndexStyle.Analyzed;
				yield return prop;
			}
		}

		/// <summary>
		/// Gets the item instances associated with the given item uri.
		/// </summary>
		/// <param name="itemUri">the uri of the item</param>
		/// <returns>an item instance located at the uri or null if none found.</returns>
		/// <exception cref="ArgumentNullException">thrown if <paramref name="itemUri"/> is <c>null</c></exception>
		/// <exception cref="InvalidOperationException">thrown if the IRequestHandler is null</exception>
		IEnumerable<IItemInstance> GetItems(Uri itemUri, bool includeVersions = false)
		{
			if (m_Handler == null)
				throw new InvalidOperationException("Can not get item because the factory was not initialized or created with a reference to the collection.");
			if (itemUri == null)
				throw new ArgumentNullException("itemUri");

			return m_Handler.ItemRequest(m_SystemUser, RequestTypes.GetItem | (includeVersions ? RequestTypes.VersionAll : RequestTypes.VersionPublished), itemUri, LoadOptions.ItemComplete);
		}

		#endregion
	}
}
